package com.jl;

import com.google.auto.service.AutoService;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableSet;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.*;
import java.lang.annotation.Annotation;
import java.util.*;

/**
 * 自动生成spring.factories文件
 */
@SupportedOptions({"debug", "verify"})
@AutoService(Processor.class)
public class JLSpringFactorles extends AbstractProcessor {

    static final String SERVICES_PATH = "META-INF/spring.factories";

    private Map<String, Set<String>> providers = new HashMap<>();

    private List<Class<? extends Annotation>> list = Arrays.asList(
            Component.class, Service.class
            , Configuration.class, RestController.class
            , FeignClient.class, RestControllerAdvice.class
    );

    @Override
    public ImmutableSet<String> getSupportedAnnotationTypes() {
        Set<String> strings = new HashSet<>();
        for (Class<? extends Annotation> classz : list) {
            strings.add(classz.getName());
        }
        ImmutableSet<String> set = ImmutableSet.copyOf(strings);
        return set;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        try {
            if (roundEnv.processingOver()) {
                generateConfigFiles();
            } else {
                list.forEach(classz -> exec(roundEnv, classz));
            }
        } catch (Exception e) {
            StringWriter writer = new StringWriter();
            e.printStackTrace(new PrintWriter(writer));
            fatalError(writer.toString());
        }
        return true;
    }

    private void exec(RoundEnvironment roundEnv, Class<? extends Annotation> classz) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(classz);
        for (Element e : elements) {
            TypeElement providerImplementer = (TypeElement) e;
            TypeElement providerType = processingEnv.getElementUtils().getTypeElement(EnableAutoConfiguration.class.getName());
            if (checkImplementer(providerImplementer, providerType)) {
                providers.computeIfAbsent(getBinaryName(providerType), key -> new HashSet<>())
                        .add(getBinaryName(providerImplementer));
            }
        }
    }

    private void generateConfigFiles() {
        Filer filer = processingEnv.getFiler();
        try {
            FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
                    SERVICES_PATH);
            OutputStream out = fileObject.openOutputStream();
            Files.writeServiceFile(providers, out);
            out.close();
            log("Wrote to: " + fileObject.toUri());
        } catch (IOException e) {
            fatalError("Unable to create " + SERVICES_PATH + ", " + e);
            return;
        }
    }

    private boolean checkImplementer(TypeElement providerImplementer, TypeElement providerType) {

        String verify = processingEnv.getOptions().get("verify");
        if (verify == null || !Boolean.valueOf(verify)) {
            return true;
        }

        Types types = processingEnv.getTypeUtils();

        return types.isSubtype(providerImplementer.asType(), providerType.asType());
    }

    private String getBinaryName(TypeElement element) {
        return getBinaryNameImpl(element, element.getSimpleName().toString());
    }

    private String getBinaryNameImpl(TypeElement element, String className) {
        Element enclosingElement = element.getEnclosingElement();

        if (enclosingElement instanceof PackageElement) {
            PackageElement pkg = (PackageElement) enclosingElement;
            if (pkg.isUnnamed()) {
                return className;
            }
            return pkg.getQualifiedName() + "." + className;
        }

        TypeElement typeElement = (TypeElement) enclosingElement;
        return getBinaryNameImpl(typeElement, typeElement.getSimpleName() + "$" + className);
    }

    private void log(String msg) {
        if (processingEnv.getOptions().containsKey("debug")) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
        }
    }

    private void fatalError(String msg) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR: " + msg);
    }


    public static class Files {
        static void writeServiceFile(Map<String, Set<String>> services, OutputStream output) throws IOException {
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, Charsets.UTF_8));
            for (Map.Entry<String, Set<String>> entry : services.entrySet()) {
                writer.write(entry.getKey() + "=\\");

                boolean first = true;
                for (String service : entry.getValue()) {
                    if (first) {
                        writer.newLine();
                        writer.write(service);
                        first = false;
                    } else {
                        writer.write(",\\");
                        writer.newLine();
                        writer.write(service);
                    }
                }
                writer.newLine();
            }
            writer.flush();
        }

    }

}